package edu.qhst.user.service.impl;

import cn.hutool.core.lang.Pair;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import edu.qhst.common.auth.JwtUtils;
import edu.qhst.common.auth.UserHolder;
import edu.qhst.common.constant.SystemConfigConsts;
import edu.qhst.common.exception.BusinessException;
import edu.qhst.common.resp.ErrorCode;
import edu.qhst.common.utils.JsonUtils;
import edu.qhst.user.constant.UserConsts;
import edu.qhst.user.dao.entity.MailInfo;
import edu.qhst.user.dao.entity.User;
import edu.qhst.user.dao.mapper.UserMapper;
import edu.qhst.user.dto.UserSafeInfoDto;
import edu.qhst.user.dto.resp.UserLoginRespDto;
import edu.qhst.user.dto.resp.UserRegisterRespDto;
import edu.qhst.user.service.MailService;
import edu.qhst.user.service.UserService;
import edu.qhst.user.utils.AlgorithmUtils;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.util.DigestUtils;

import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import static edu.qhst.user.constant.RedisConsts.*;

/**
 * 用户服务实现类
 *
 * @author 加浓美式
 * @Date 2023/6/21
 */
@Service
@Slf4j
@RequiredArgsConstructor
public class UserServiceImpl extends ServiceImpl<UserMapper, User>
        implements UserService {

    private final UserMapper userMapper;

    private final MailService mailService;

    private final RabbitTemplate rabbitTemplate;

    private final RedisTemplate<String, Object> redisTemplate;

    @Override
    public UserRegisterRespDto userRegister(String userAccount, String username, String userClass, String phone, int gender, String email) {
        // 1. 校验
        if (StringUtils.isAnyBlank(userAccount, username, userClass, phone, email)) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR, "参数为空");
        }
        if (userAccount.length() < 10 || phone.length() < 10) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR, "参数错误");
        }
        // 账户不能包含特殊字符
        String validPattern = "[`~!@#$%^&*()+=|{}':;',\\\\[\\\\].<>/?~！@#￥%……&*（）——+|{}【】‘；：”“’。，、？]";
        Matcher matcher = Pattern.compile(validPattern).matcher(userAccount);
        if (matcher.find()) {
            return null;
        }
        // 账户不能重复
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("userAccount", userAccount);
        User getUser = userMapper.selectOne(queryWrapper);
        if (getUser != null && getUser.getUserStatus() == 0) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR, "该学号已注册");
        }
        if (getUser != null && getUser.getUserStatus() == 1) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户已提交申请,请勿重复提交");
        }
        // 申请被打回后重新申请
        if (getUser != null && getUser.getUserStatus() == 2) {
            getUser.setUserStatus(1);
            userMapper.updateById(getUser);
            return UserRegisterRespDto.builder()
                    .uid(getUser.getId())
                    .build();
        }
        // 2. 加密
        String encryptPassword = DigestUtils.md5DigestAsHex((UserConsts.SALT + userAccount).getBytes());
        // 3. 插入数据
        User user = new User();
        user.setUserAccount(userAccount);
        user.setUserName(username);
        user.setUserPassword(encryptPassword);
        user.setUserClass(userClass);
        user.setEmail(email);
        user.setGender(gender);
        user.setPhone(phone);
        user.setUserRole(0);
        user.setUserStatus(1); // 1 - 待管理员审核
        boolean saveResult = this.save(user);
        if (!saveResult) {
            return null;
        }
        return UserRegisterRespDto.builder()
                .uid(user.getId())
                .build();
    }

    /**
     * 管理员审核用户申请
     *
     * @param userId
     * @param option  1 - 通过  0 - 打回
     * @param message
     * @return option
     */
    @Override
    public int userAudit(int userId, int option, String message) {
        User user = userMapper.selectById(userId);
        String name = user.getUserName();
        String email = user.getEmail();
        MailInfo mailInfo = new MailInfo();
        mailInfo.setMail(email);
        mailInfo.setName(name);
        mailInfo.setMessage(message);
        if (option == 1) {
            user.setUserStatus(0);
            this.updateById(user);
            mailInfo.setType(1);
            String mailDate = JsonUtils.serialize(mailInfo);
            assert mailDate != null;
            rabbitTemplate.convertAndSend("mailExchange", "user.mail", mailDate);
//            mailService.sendHtmlMail(user.getEmail(), "启航社团加入申请回复", MailConsts.getOKMailMsg(name));
        } else {
            user.setUserStatus(2);
            this.updateById(user);
            mailInfo.setType(0);
            String mailDate = JsonUtils.serialize(mailInfo);
            assert mailDate != null;
            rabbitTemplate.convertAndSend("mailExchange", "user.mail", mailDate);
//            mailService.sendHtmlMail(user.getEmail(), "启航社团加入申请回复", MailConsts.getFailMailMsg(name, message));
        }
        return option;
    }

    @Override
    public UserLoginRespDto userLogin(String userAccount, String userPassword, HttpServletRequest request) {
        // 1. 校验
        if (StringUtils.isAnyBlank(userAccount, userPassword)) {
            return null;
        }
        if (userAccount.length() < 4) {
            return null;
        }
        if (userPassword.length() < 8) {
            return null;
        }
        String address = request.getRemoteAddr();
        // 账户不能包含特殊字符
        String validPattern = "[`~!@#$%^&*()+=|{}':;',\\\\[\\\\].<>/?~！@#￥%……&*（）——+|{}【】‘；：”“’。，、？]";
        Matcher matcher = Pattern.compile(validPattern).matcher(userAccount);
        if (matcher.find()) {
            return null;
        }
        // 2. 加密
        String encryptPassword = DigestUtils.md5DigestAsHex((UserConsts.SALT + userPassword).getBytes());
        // 查询用户是否存在
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("userAccount", userAccount);
        queryWrapper.eq("userPassword", encryptPassword);
        User user = userMapper.selectOne(queryWrapper);
        // 用户不存在
        if (user == null) {
            log.info("user login failed, userAccount cannot match userPassword");
            throw new BusinessException(ErrorCode.LOGIN_ERROR, "密码错误");
        }
        if (user.getUserStatus() != 0) {
            throw new BusinessException(ErrorCode.LOGIN_ERROR, "用户未审核通过");
        }
        // JWT在黑名单中，则删除
        if(Boolean.TRUE.equals(redisTemplate.hasKey(JWT_BLACKLIST + user.getId()))) redisTemplate.delete(JWT_BLACKLIST + user.getId());
        // 生成JWT并返回
        return UserLoginRespDto.builder()
                .userName(user.getUserName())
                .uid(user.getId())
                .token(JwtUtils.generateToken(user.getId(), SystemConfigConsts.QHST_FRONT_KEY, address))
                .build();
    }

    @Override
    public boolean changePwd(String oldPwd, String newPwd, HttpServletRequest request){
        String encryptOldPwd = DigestUtils.md5DigestAsHex((UserConsts.SALT + oldPwd).getBytes());
        String encryptNewPwd = DigestUtils.md5DigestAsHex((UserConsts.SALT + newPwd).getBytes());
        Long userId = UserHolder.getUserId();
        User user = userMapper.selectById(userId);
        String userPassword = user.getUserPassword();
        if(!encryptOldPwd.equals(userPassword)) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR, "原密码错误");
        }
        user.setUserPassword(encryptNewPwd);
        this.updateById(user);
        redisTemplate.opsForValue().setIfAbsent(JWT_BLACKLIST + user.getId(), request.getHeader(SystemConfigConsts.HTTP_AUTH_HEADER_NAME));
        return true;
    }

    /**
     * 用户脱敏
     *
     * @param originUser
     * @return
     */
    @Override
    public UserSafeInfoDto getSafetyUser(User originUser) {
        if (originUser == null) {
            return null;
        }
        UserSafeInfoDto safetyUser = new UserSafeInfoDto();
        safetyUser.setId(originUser.getId());
        safetyUser.setUserName(originUser.getUserName());
        safetyUser.setUserAccount(originUser.getUserAccount());
        safetyUser.setAvatarUrl(originUser.getAvatarUrl());
        safetyUser.setGender(originUser.getGender());
        safetyUser.setProfile(originUser.getProfile());
        safetyUser.setPhone(originUser.getPhone());
        safetyUser.setEmail(originUser.getEmail());
        safetyUser.setUserClass(originUser.getUserClass());
        safetyUser.setUserRole(originUser.getUserRole());
        safetyUser.setTags(originUser.getTags());
        return safetyUser;
    }


    /**
     * 用户注销
     *
     * @param request
     */
    @Override
    public boolean userLogout(HttpServletRequest request) {
        UserSafeInfoDto currentUser = getCurrentUser(request);
        // JWT加入黑名单
        redisTemplate.opsForValue().setIfAbsent(JWT_BLACKLIST + currentUser.getId(), request.getHeader(SystemConfigConsts.HTTP_AUTH_HEADER_NAME));
        return true;
    }


    /**
     * 根据标签查询用户 (内存过滤 )
     *
     * @param tagNameList 标签查询条件
     * @return
     */
    @Override
    public List<UserSafeInfoDto> searchUsersByTags(List<String> tagNameList) {
        // 判断是否为空
        if (CollectionUtils.isEmpty(tagNameList)) {
            // 为空，抛出异常 - 请求参数错误
            throw new BusinessException(ErrorCode.PARAMS_ERROR);
        }
        //内存查询
        // 1.先查询所有用户
        List<User> userList = query().list();
        Gson gson = new Gson();
        // 2.在内存中判断是否包含要求的标签
        return userList.stream().filter(user -> {
            String tagStr = user.getTags();
//            if (StringUtils.isEmpty(tagStr)){
//                return false;
//            }
            Set<String> tempTagNameSet = gson.fromJson(tagStr, new TypeToken<Set<String>>() {
            }.getType());
            //判空
            tempTagNameSet = Optional.ofNullable(tempTagNameSet).orElse(new HashSet<>());
            for (String tagName : tagNameList) {
                if (!tempTagNameSet.contains(tagName)) {
                    return false;
                }
            }
            return true;
        }).map(this::getSafetyUser).collect(Collectors.toList());
    }


    /**
     * 更新用户信息
     *
     * @param userInfo
     * @param loginUser
     * @return
     */
    @Override
    public int updateUser(UserSafeInfoDto userInfo, UserSafeInfoDto loginUser) {
        long userId = userInfo.getId();
        if (userId <= 0) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR);
        }
        //仅管理员和自己可以修改
        //管理员 允许更新任意用户
        //非管理员 只允许更新当前(自己的)信息
        if (!isAdmin(loginUser) && userId != loginUser.getId()) {
            throw new BusinessException(ErrorCode.NO_AUTH);
        }
        User oldUser = this.getById(userInfo.getId());
        if (oldUser == null) {
            throw new BusinessException(ErrorCode.NULL_ERROR);
        }
        oldUser.setUserAccount(userInfo.getUserAccount());
        oldUser.setUserName(userInfo.getUserName());
        oldUser.setAvatarUrl(userInfo.getAvatarUrl());
        oldUser.setUserClass(userInfo.getUserClass());
        oldUser.setEmail(userInfo.getEmail());
        oldUser.setGender(userInfo.getGender());
        oldUser.setTags(userInfo.getTags());
        oldUser.setPhone(userInfo.getPhone());
        oldUser.setProfile(userInfo.getProfile());
        return userMapper.updateById(oldUser);
    }


    /**
     * 获取当前用户
     *
     * @param request
     * @return
     */
    @Override
    public UserSafeInfoDto getCurrentUser(HttpServletRequest request) {
        // 获取登录 JWT
        String token = request.getHeader(SystemConfigConsts.HTTP_AUTH_HEADER_NAME);

        // 开始认证
        if (!org.springframework.util.StringUtils.hasText(token)) {
            // token 为空
            throw new BusinessException(ErrorCode.LOGIN_ERROR);
        }
        Long userId = JwtUtils.parseToken(token, SystemConfigConsts.QHST_FRONT_KEY);
        if (Objects.isNull(userId)) {
            // token 解析失败
            throw new BusinessException(ErrorCode.LOGIN_ERROR);
        }
        User user = userMapper.selectById(userId);
        return getSafetyUser(user);
    }

    @Override
    public UserSafeInfoDto getUserById(Long userId) {
        User user = userMapper.selectById(userId);
        return getSafetyUser(user);
    }

    /**
     * 是否为管理员
     *
     * @param request
     * @return
     */
    @Override
    public boolean isAdmin(HttpServletRequest request) {
        UserSafeInfoDto user = getCurrentUser(request);
        return user.getUserRole() == 1;
    }


    /**
     * 是否为管理员
     *
     * @param loginUser
     * @return
     */
    @Override
    public boolean isAdmin(UserSafeInfoDto loginUser) {
        return loginUser != null && loginUser.getUserRole() == UserConsts.ADMIN_ROLE;
    }


    /**
     * 根据标签查询用户 (SQL 查询版)
     *
     * @param tagNameList
     * @return
     */
    @Deprecated
    private List<UserSafeInfoDto> searchUserByTagsBySQL(List<String> tagNameList) {
        // 判断是否为空
        if (CollectionUtils.isEmpty(tagNameList)) {
            // 为空，抛出异常 - 请求参数错误
            throw new BusinessException(ErrorCode.PARAMS_ERROR);
        }
        // 不为空
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        //拼接 and 查询 like '%标签1%' and like '%标签2%'
        for (String tagName : tagNameList) {
            queryWrapper = queryWrapper.like("tags", tagName);
        }
        List<User> userList = userMapper.selectList(queryWrapper);
        return userList.stream().map(this::getSafetyUser).collect(Collectors.toList());
    }


    /**
     * 获取最匹配的用户
     *
     * @param num
     * @param loginUser
     * @return
     */
    @Override
    public List<UserSafeInfoDto> matchUsers(long num, UserSafeInfoDto loginUser) {
        List<User> userList = this.query().select("id", "tags").isNotNull("tags").list();
        String tags = loginUser.getTags();

        Gson gson = new Gson();
        List<String> tagList = gson.fromJson(tags, new TypeToken<List<String>>() {
        }.getType());
        // 用户列表的下标 => 相似度
        List<Pair<User, Long>> list = new ArrayList<>();
        // 依次计算所有用户和当前用户的相似度
        for (int i = 0; i < userList.size(); i++) {
            User user = userList.get(i);
            String userTags = user.getTags();
            // 无标签或者为当前用户自己
            if (StringUtils.isBlank(userTags) || user.getId() == loginUser.getId()) {
                continue;
            }
            List<String> userTagList = gson.fromJson(userTags, new TypeToken<List<String>>() {
            }.getType());
            // 计算分数
            long distance = AlgorithmUtils.minDistance(tagList, userTagList);
            list.add(new Pair<>(user, distance));
        }
        // 按编辑距离由小到大排序
        List<Pair<User, Long>> topUserPairList = list.stream()
                .sorted((a, b) -> (int) (a.getValue() - b.getValue()))
                .limit(num)
                .collect(Collectors.toList());
        // 排好顺序的 userId 列表
        List<Long> userIdList = topUserPairList.stream()
                .map(pair -> pair.getKey().getId()).collect(Collectors.toList());
        QueryWrapper<User> userQueryWrapper = new QueryWrapper<>();
        userQueryWrapper.in("id", userIdList);
        // 脱敏用户信息，并按id分组
        // 1 => User1, 2 => User2, 3 => User3
        Map<Long, List<UserSafeInfoDto>> userIdUserListMap = this.list(userQueryWrapper)
                .stream()
                .map(user -> getSafetyUser(user))
                .collect(Collectors.groupingBy(UserSafeInfoDto::getId));
        List<UserSafeInfoDto> finalUserList = new ArrayList<>();
        for (Long userId : userIdList) {
            finalUserList.add(userIdUserListMap.get(userId).get(0));
        }
        return finalUserList;
    }

    @Override
    public List<User> searchUsersByStatus() {
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("userStatus", 1);
        return userMapper.selectList(queryWrapper);
    }

    @Override
    public List<Integer> selectUserIdByTeam(int TeamId) {
        return userMapper.selectUserIdByTeam(TeamId);
    }

    @Override
    public Page<UserSafeInfoDto> recommendUsers(long pageSize, long pageNum, HttpServletRequest request) {
        UserSafeInfoDto currentUser = getCurrentUser(request);
        String redisKey = RECOMMEND_KEY + currentUser.getId();
        //查询redis
        Page<UserSafeInfoDto> userPage = (Page<UserSafeInfoDto>) redisTemplate.opsForValue().get(redisKey);
        //有缓存, 直接读缓存
        if (userPage != null) {
            return userPage;
        }
        userPage = new Page<>();
        //无缓存, 查数据库
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        List<User> list = this.page(new Page<>(pageNum, pageSize), queryWrapper).getRecords();
        List<UserSafeInfoDto> userSafeInfoDtoList = new ArrayList<>();
        for(User user: list) {
            UserSafeInfoDto userSafeInfoDto = getSafetyUser(user);
            userSafeInfoDtoList.add(userSafeInfoDto);
        }
        userPage.setRecords(userSafeInfoDtoList);
        //将查到的主页推荐用户信息写入缓存
        try {
            redisTemplate.opsForValue().set(redisKey, userPage, RECOMMEND_TTL, TimeUnit.SECONDS);
        } catch (Exception e) {
            log.error("redis set key error", e);
            throw new BusinessException(ErrorCode.SYSTEM_ERROR);
        }
        return userPage;
    }

    @Override
    public List<UserSafeInfoDto> listUserInfoByIds(List<Long> userIds) {
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        queryWrapper.in("id", userIds);
        return userMapper.selectList(queryWrapper).stream().map(v -> getSafetyUser(v)).collect(Collectors.toList());
    }
}

